import os
import time
import json
import logging
import socket

from django.db import models
from django.forms import ModelForm

from autobbotd.bbconfig import *
from buildbotweb.buildbot.btester import *
from autobbotd.html2text import html2text

#from mailtools.mailsmtp import mail_smtp
from mailtools.mailnntp import mail_nntp
from mailtools.messenger import Messenger

# Setup dummy logging.
logger = logging.getLogger('model_logger')
logger.setLevel(logging.DEBUG)
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
logger.addHandler(ch)

# Create your models here.

class TestPaths(models.Model):
    testpath = models.CharField(
            max_length=200,
            default="(?P<srcpath>^mybranch)/.*",
            help_text="Regex to look for"
    )
    def __unicode__(self):
        return self.testpath

class TestStatuses(models.Model):
    teststatus = models.CharField(max_length=50, editable=False)
    def __unicode__(self):
        return "%s" % self.teststatus

class TestTypes(models.Model):
    testtype = models.CharField(max_length=200, editable=False)
    def __unicode__(self):
        return self.testtype

class BTester(models.Model):
    logfile = models.CharField(
            max_length=200,
            default='/var/log/buildbot.log', 
            help_text='Logfile for the buildbot server'
    )
    releasedir = models.CharField(
            max_length=200,
            help_text='Location to build everything'
    )
    svnuser = models.CharField(
            max_length=32,
            help_text='SVN username'
    )
    svnrepo = models.CharField(
            max_length=100,
            help_text='Location of SVN repository'
    )
    mailto = models.CharField(
            max_length=400,
            help_text='Admin Email Address'
    )
    mailfrom = models.CharField(
            max_length=400,
            help_text='Who will be the sender?'
    )
    mailhost = models.CharField(
            max_length=100,
            help_text='Email host'
    )
    mailport = models.IntegerField(default=25)
    newshost = models.CharField(
            max_length=100,
            blank=True,
            help_text='Host for newsgroup server'
    )
    newsgroup = models.CharField(
            max_length=100,
            blank=True,
            help_text='Name of the newsgroup'
    )
    newsport = models.IntegerField(default=119)
    newsuser = models.CharField(
            max_length=100,
            blank=True,
            help_text='Username for newsgroup'
    )
    newspwd = models.CharField(
            max_length=25,
            blank=True,
            help_text='Newsgroup password'
    )
 
    def __init__(self, *args, **kwds):
        models.Model.__init__(self, *args, **kwds)
        self.messenger = Messenger()

    def __unicode__(self):
        return "%s %s %s" % (self.releasedir, self.svnuser, self.svnrepo)

    def sendnews(
        self,
        bodyfile,
        nntpfrom,
        subject,
        maxlines=None
    ):

        if not self.newshost:
            logger.info("Empty newshost, will not send news.")

        if maxlines:
            maxdata = tailFile(bodyfile, maxlines)
            h, tpath = tempfile.mkstemp(text=True)
            tfile = os.fdopen(h, "w")
            try:
                tfile.write(maxdata)
            finally:
                tfile.close()

            bodyfile = tpath
       
        try:
            mail_nntp(
                nntpfrom,
                self.newsgroup,
                subject,
                self.newshost,
                self.newsport,
                self.newsuser,
                self.newspwd,
                bodyfile
            )
        except Exception, err:
            logger.error("Failed to send news.")
            logger.exception(err)
            
        if maxlines:
            os.unlink(tpath)

    def sendemail(
        self,
        subject,
        mailtos,
        bodyfile=None,
        maxlines=None,
        bodytext="",
        attachments=[]
    ):

        if bodyfile:
            f = open(bodyfile)
            try:
                if maxlines:
                    # Assuming 80 char lines
                    maxbytes = 80 * maxlines
                    try:
                        f.seek(-maxbytes, 2)
                    except IOError:
                        f.seek(0)
                bodytext = f.read()
            finally:
                f.close()

        self.messenger.send_email(
            self.mailhost,
            mailtos,
            self.mailfrom,
            subject,
            bodytext,
            attachments
        )

class BTesterForm(ModelForm):
    class Meta:
        model = BTester


class Tests(models.Model):
    testname = models.CharField(max_length=200, help_text="Name of your test")
    testtype = models.ForeignKey(TestTypes, help_text="What type is it?")
    testcmd = models.TextField(
            help_text="Shell command to run.", 
            default="CHROOTDIR=%s\n\n"
    )
    logpath = models.CharField(max_length=200, help_text="Name of the log file")
    errpath = models.CharField(max_length=200, help_text="Name of the error log")
    def __unicode__(self):
        return "%s %s" % (self.id, self.testname)
    
    def run(self, ts, tsr, logger=None):
        tr = TestRuns()
        tr.test = self
        tr.testsuiterun = tsr
        ret = False
        try:
            ret = tr.start()
        finally:
            tr.stop(ret)
            ts.btester.sendnews(
                "%s_%s" % (tsr.chrootdir, self.logpath),        
                "BuildNTest",
                "[BUILDNTEST] %s Test %s on %s Results %s" % (
                    "Pass" if ret else "FAIL!",
                    self.testname,
                    os.path.basename(tsr.chrootdir),
                    tsr.svnrev 
                ),
                maxlines=500
            )

            if not ret:
                ts.btester.sendemail(
                    "[BUILDNTEST] Test %s on %s Failed at %s" % (
                        self.testname,
                        os.path.basename(tsr.chrootdir), 
                        tsr.svnrev 
                    ),
                    ts.mailto,
                    bodyfile = "%s_%s" % (tsr.chrootdir, self.logpath),
                    maxlines = 500,
                    attachments = [
                        "%s_%s" % (tsr.chrootdir, self.logpath), 
                        "%s_%s" % (tsr.chrootdir, self.errpath)
                    ]
                )
            return ret


class TestSuites(models.Model):
    testsuite = models.CharField(max_length=100, help_text="Name of the test suite")
    testpath = models.ForeignKey(TestPaths, help_text="Path regex to match on")
    srcdir = models.CharField(
        max_length=200, 
        blank=True,
        help_text="This will be provided by the testpath regex if available."
    )
    releasedir = models.CharField(max_length=200, help_text="Location to build chroot.")
    buildcmd = models.TextField(
            help_text="Command to perform chroot build of product",
            default="""# Params:
# ${1} srcpath
# ${2} releasedir
# ${3} revision

SRCPATH=%s
RELDIR=%s
REV=%s"""
    )
    mailto = models.CharField(max_length=400, help_text="Who should receive email updates?")
    btester = models.ForeignKey(BTester)
    tests = models.ManyToManyField(Tests, through="TestSuiteTests")

    def __unicode__(self):
        return self.testsuite
    
    def run(self, srcpath, logger=None):
        """ Run

        Start executing the test suite.  Go through a few distinct phases:
         * Build the code
         * Gather tests for the suite
         * Execute tests
         * Retrieve data
        """

        if logger:
            logger.info("New and more Djangoy!")
            logger.info("Running suite: %s" % self.testsuite)
        #return "Running suite: %s" % self.testsuite

        # Setup some parameters
        svnpath = "%s/%s" % (SVNREPO, srcpath)
        author, revision, revdate = getSvnPathInfo(svnpath)
        srcbase = os.path.basename(srcpath)
        if not os.path.isdir(self.releasedir):
            os.makedirs(self.releasedir)
        chrootdir = os.path.join(self.releasedir, srcbase)


        # Mark the suite as started running
        tsr = TestSuiteRuns()
        tsr.testsuite_id = self.id
        tsr.chrootdir = chrootdir
        tsr.author = author
        tsr.svnrev = revision
        tsr.testtime = time.strftime("%Y-%m-%d %H:%M:%S")
        tsr.teststatus_id = 2
        tsr.save()

        # Drop the plinko chip
        ret = False
        try:
            ret = tsr.start(
                svnpath,
                logger
            )
        finally:
            # Log build results
            tsr.stop(ret)

            buildlog = "%s_build.log" % chrootdir 
            self.btester.sendnews(
                buildlog,
                "BuildNTest",
                "[BUILDNTEST] %s TestSuite %s Results %s" % (
                    "Pass" if ret else "FAIL!",
                    self.testsuite,
                    revision
                ),
                500
            )

            self.btester.sendemail(
                "[BUILDNTEST] TestSuite %s %s at r%s" % (
                    self.testsuite, 
                    "Passed" if ret else "Failed",
                    revision
                ),
                self.mailto,
                #bodyfile=buildlog,
                bodytext = html2text(str(testsuiterun_details('',tsr.id))),
                attachments=buildlog,
                maxlines=500
            )


class TestSuiteTests(models.Model):
    testsuite = models.ForeignKey(TestSuites)
    test = models.ForeignKey(Tests)
    def __unicode__(self):
        return "%s:%s" % (self.testsuite, self.test)

class TestSuiteRuns(models.Model):
    testsuite = models.ForeignKey(TestSuites)
    teststatus = models.ForeignKey(TestStatuses)
    chrootdir = models.CharField(max_length=200)
    author = models.CharField(max_length=100)
    svnrev = models.IntegerField()
    testtime = models.DateTimeField('time started')
    logtail = models.TextField(blank=True, editable=False)

    def __unicode__(self):
        return "%s %s %s %s %s" % (
            self.svnrev, 
            self.author, 
            self.teststatus,
            self.testtime,
            self.testsuite
        )

    def start(self, svnpath, logger):
        self.teststatus_id = 4
        self.save()

        self.testsuite.btester.sendemail(
            "[BUILDNTEST] Starting TestSuite %s at r%s" % (
                self.testsuite, 
                self.svnrev
            ),
            self.testsuite.mailto,
            bodytext="""See /testsuiteruns/%s
for more details.
            %s""" % (
                self.id, 
                html2text(str(testsuiterun_details('',self.id))),
            )      
        )

        buildret = self._buildProduct(
            self.testsuite.buildcmd,
            svnpath,
            self.testsuite.releasedir,
            logger
        )

        if not buildret:
            return False
                
        # Gather tests
        tests = Tests.objects.filter(testsuitetests__testsuite = self.testsuite.id)

        # Run each test
        passAllTests = True
        for test in tests:
            ret = False
            ret = test.run(self.testsuite, self, logger)
            if not ret:
                passAllTests = False

        return passAllTests

    def _buildProduct(self, buildcmd, srcpath, releasedir, logger=None):
        """
        buildProduct
            srcdir - Location of the source checkout
            chrootdir - Location to use as the chroot

        Build the product in a chroot
        """
        if logger:
            logger.debug("In buildProduct")
            logger.debug("CHROOTDIR: %s" % self.chrootdir)

        #self.killct(chrootdir)

        cmd = buildcmd % (
            srcpath,
            releasedir,
            self.svnrev
        )
        cmd = cmd.replace("\r\n", "\n")

        if logger:
            logger.debug("CMD: %s" % cmd)

        f = open("%s_build.log" % self.chrootdir, "w")
        try:
            proc = Popen(
                cmd,
                shell=True,
                stdout=f,
                stderr=f
            )
            if logger:
                logger.info("Running build...")

            self.getLog()
            while proc.poll() is None:
                time.sleep(5)
                self.getLog()
        finally:
            f.close()
        ret = proc.returncode
        
        if logger:
            logger.info("Done with build. RET: %s" % ret)

        if ret == 0:
            buildret = True
        else:
            buildret = False

        return buildret

    def stop(self, result):
        if result:
            self.teststatus_id = 3
        else:
            self.teststatus_id = 1
        self.save()
        self.getLog()

    def getLog(self):
        """
        getLog Retrieve the logfile for the TestSuiteRun
        """
        buildlog = "%s_build.log" % self.chrootdir 
        self.logtail = tailFile(buildlog, 50)
        try:
            self.save()
        except OperationalError:
            logger.warning("Database appears to be locked.")


class TestRuns(models.Model):
    test = models.ForeignKey(Tests)
    testsuiterun = models.ForeignKey(TestSuiteRuns)
    testtime = models.DateTimeField('time started')
    teststatus = models.ForeignKey(TestStatuses)
    logtail = models.TextField(blank=True, editable=False)
    errtail = models.TextField(blank=True, editable=False)

    def __unicode__(self):
        return "%s %s %s %s %s" % (self. id, self.testtime, self.test, self.testsuiterun, self.teststatus)
    
    def _run(self):
        """
        run

        Execute the given testcmd.
        """
        logpath = "%s_%s" % (self.testsuiterun.chrootdir, self.test.logpath)
        errpath = "%s_%s" % (self.testsuiterun.chrootdir, self.test.errpath)
        flog = open(logpath, "w")
        ferr = open(errpath, "w")

        testcmd = self.test.testcmd % self.testsuiterun.chrootdir
        testcmd = testcmd.replace("\r\n", "\n")

        logger.debug("TEST CMD: %s" % testcmd)

        try:
            proc = Popen(
                testcmd, 
                shell = True,
                stdout = flog,
                stderr = ferr
            )
            self.getLog()
            while proc.poll() is None:
                time.sleep(5)
                self.getLog()
        finally:
            if flog:
                flog.close()
            if ferr:
                ferr.close()
        
        ret = proc.returncode
        if ret == 0:
            return True
        else:
            return False

    def start(self):
        self.testtime = time.strftime("%Y-%m-%d %H:%M:%S")
        self.teststatus_id = 4
        self.save()
        return self._run()

    def stop(self, result):
        if result:
            self.teststatus_id = 3
        else:
            self.teststatus_id = 1
        self.save()
        self.getLog()
        self.getTestData()

    def getTestData(self):
        result_path = os.path.join(self.testsuiterun.chrootdir, '.results')
        if not os.path.isfile(result_path):
            return

        if self.test.testtype_id == 2:
            u = UnitTestResults()
            u.testrun = self 
            u.saveData(result_path)
            u.testForRegression(self.testsuiterun.chrootdir)
        elif self.test.testtype_id == 1:
            m = MailTestResults()
            m.testrun = self
            m.saveData(result_path)
        else:
            h = open(result_path)
            try:
                data = eval(h.read())
            finally:
                h.close()
            for key in data:
                try:
                    testresult = TestResults()
                    testresult.testrun = self
                    testresult.result_name = key
                    testresult.result_value = data.get(key)
                    testresult.save()
                except AttributeError, TypeError:
                    continue
                
        os.remove(result_path)


    def getLog(self):
        """
        getLog Retrieve the logfile for the TestRun
        """
        testlog = "%s_%s" % (self.testsuiterun.chrootdir, self.test.logpath)
        testerr = "%s_%s" % (self.testsuiterun.chrootdir, self.test.errpath)
        data = tailFile(testlog, 50)
        self.logtail = data 
        self.errtail = tailFile(testerr, 50)
        self.save()


class TestResults(models.Model):
    testrun = models.ForeignKey(TestRuns)
    result_name = models.CharField(max_length=50)
    result_value = models.IntegerField()

    def __unicode__(self):
        return  "%s %s %s %s" % (self.id, self.result_name, self.result_value, self.testrun.id)

class MailTestResults(models.Model):
    testrun = models.ForeignKey(TestRuns)
    recv_bytes_sec = models.IntegerField()
    send_msgs_sec = models.IntegerField()
    recv_msgs_sec = models.IntegerField()
    recv_latency = models.IntegerField()
    msgsize = models.IntegerField()
    send_threads = models.IntegerField()
    logpath = models.CharField(max_length=200)

    def __unicode__(self):
        return "RunID: %s, rbytes_sec:%s, rmsgs_sec:%s, msg_size:%s" % (
            self.testrun_id,
            self.recv_bytes_sec,
            self.recv_msgs_sec,
            self.msgsize
        )

    def saveData(self, result_path):
        h = open(result_path)
        try:
            data = h.read()
        finally:
            h.close()

        if not data:
            return
        
        try:
            result_data = json.loads(data)
        except TypeError:
            result_data = {}

        for result in result_data.get('results'):
            for datum in result.get('data'):
                self.recv_bytes_sec = int(datum.get('bytes_sec', -1))
                self.send_msgs_sec = int(datum.get('send_msgs_sec', -1))
                self.recv_msgs_sec = int(datum.get('recv_msgs_sec', -1))
                self.msgsize = int(datum.get('msgsize', -1))
                self.send_threads = int(datum.get('send_threads', -1))
                self.recv_latency = int(datum.get('recv_latency', -1))
                self.save()


class UnitTestResults(models.Model):
    testrun = models.ForeignKey(TestRuns)
    numtests = models.IntegerField()
    numfailures = models.IntegerField()
    numerrors = models.IntegerField()
    statements = models.IntegerField()
    statements_executed = models.IntegerField()
    logpath = models.CharField(max_length=200)

    def __unicode__(self):
        return "RunID:%s, Tests:%s, Failures:%s, Errors:%s, %%Covered:%0.2f%%" % (
            self.testrun_id,
            self.numtests,
            self.numfailures,
            self.numerrors,
            (float(self.statements_executed) / float(self.statements)) * 100
        )

    def saveData(self, result_path):
        h = open(result_path)
        try:
            data = eval(h.read())
        finally:
            h.close()
         
        self.numtests = int(data.get('tests', -1))
        self.numfailures = int(data.get('failures', -1))
        self.numerrors = int(data.get('errors', -1))
        self.statements = int(data.get('stmts', -1))
        self.statements_executed = int(data.get('tested_stmts', -1))
        self.logpath = " "
        self.save()

    def testForRegression(self, chrootdir):
        # Get the testsuite_id for the results we saved.
        testsuite_id = self.testrun.testsuiterun.testsuite.id

        # Now figure out the previous test results for the same test suite
        # the Django'y way.
        testsuite_set = TestSuites.objects.get(id=testsuite_id)
        testsuite_runs = testsuite_set.testsuiteruns_set.all().order_by('id').reverse()
        revision = testsuite_runs[0].svnrev

        # Make sure there is a previous run of the testsuite.
        if len(testsuite_runs) <= 1:
            return

        prevsuite_run = testsuite_runs[1]
        prevsuite_run_id = prevsuite_run.id
        
        # Got the prevsuite_run_id, now let's get the runs of pyunit style tests
        # for this testsuiterun id.

        pyunittest_runs = TestRuns.objects.filter(
            testsuiterun = prevsuite_run_id,
            test__testtype = 2
        )

        try:
            # Let's be dumb for the moment and just use one unittest style test per 
            # suite
            pyunittest_run = pyunittest_runs[0]
        except IndexError:
            print "Could not find previous unittest run."
            return 1

        regressions = []
        unittest_results = pyunittest_run.unittestresults_set.all()
        if unittest_results:
            prevtest_results = unittest_results[0]

            if prevtest_results.numtests > self.numtests:
                regressions.append("Lowered number of test cases")
            if prevtest_results.numfailures < self.numfailures:
                regressions.append("Increased failing tests")
            if prevtest_results.numerrors < self.numerrors:
                regressions.append("Increased test errors")

        if regressions:
            testlog = "%s_%s" % (
                self.testrun.testsuiterun.chrootdir, 
                self.testrun.test.logpath
            )
            print regressions
            reg_text = ", ".join(regressions)
            self.testrun.testsuiterun.testsuite.btester.sendemail(
                "Code Regressions on %s at r%s" % (
                    os.path.basename(chrootdir), 
                    self.testrun.testsuiterun.svnrev
                ),
                self.testrun.testsuiterun.testsuite.mailto,
                bodytext = """The following regressions were detected: %s.""" % reg_text,
                attachments = [testlog]
            )


# Move the circular import to the end of the module that the stuff in models is
# fully populated before anything from views is imported.
from buildbotweb.buildbot.views import testsuiterun_details
